package com.bhh.nio.chat;

import lombok.extern.slf4j.Slf4j;

import java.io.Closeable;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.nio.charset.Charset;
import java.util.Set;

/**
 * @author bhh
 * @description 关于BIO模型的服务器端实现
 * 和 ChatHandler类
 * 和客户端 ChatClient类 + UserInputHandler类配合使用
 * @date Created in 2021-04-07 15:08
 * @modified By
 */
@Slf4j
public class NIOChatServer {

    private static final int PORT = 8888;
    private static final String QUIT = "quit";
    private static final int BUFFER = 1024;

    private ServerSocketChannel serverSocketChannel;
    private Selector selector;
    /* 为什么用俩个Buffer, 为了降低耦合性,
     俩个Buffer使用场景 :
     rBuffer适用在从 Channel中读取数据,
          将数据写入 rBuffer, 经过Charset进行解码, 返回String
     wBuffer适用在向 Channel中写入数据,
          先将数据经过 Charset进行编码, 然后 put到 wBuffer, 最后写入Channel*/


    private ByteBuffer rBuffer;  // 读取ClientChannel使用的Buffer
    private ByteBuffer wBuffer; // 写入/转发ClientChannel使用的Buffer
    private Charset charset;
    private int port; //存储用户自定义的port

    NIOChatServer(int port) {
        this.port = port;
        rBuffer = ByteBuffer.allocate(BUFFER);
        wBuffer = ByteBuffer.allocate(BUFFER);
        charset = Charset.forName("UTF-8");
    }

    NIOChatServer() {
        this(PORT);
    }

    // 这里不需要map的原因在于,
    // 可以使用wBuffer来读取所有连接到Selector的ClientChannel
    // private Map<Integer, Writer> map;

    /**
     * 判断用户输入信息是否是 "quit", 是否是需要退出聊天
     *
     * @param msg
     * @return
     */
    public boolean quit(String msg) {
        return QUIT.equals(msg);
    }

    /**
     * 关闭 Closeable
     * 在这里主要关闭的是selector, 只要selector关闭, 与之相关的channel也都会关闭
     *
     * @param closeable 接口, 一般流和通道都会继承该接口
     */
    private void close(Closeable closeable) {
        try {
            closeable.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 开始方法, 整合各个模块
     */
    public void start() {
        try {
            // 初始化ServerSocketChannel
            serverSocketChannel = ServerSocketChannel.open();
            // 设置ServerSocketChannel 调用 为不阻塞方式
            serverSocketChannel.configureBlocking(false);
            // 绑定端口, 注意并不是直接由通道来绑定端口, 还是由socket来绑定端口
            // socket()返回ServerSocket对象, 再调用bind() 方法来绑定端口
            serverSocketChannel.socket().bind(new InetSocketAddress(port));

            // 初始化selector
            selector = Selector.open();
            // 将ServerSocketChannel 的 ACCEPT事件注册到 selector 中
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
            log.info("服务器已经成功启动, 监听端口 : " + port);

            Set<SelectionKey> selectionKeys = null;
            while (true) {
                // select()方法本身是会造成阻塞的
                // 若是所有注册在selector中的channel指定的事件都没有发生,
                // 则一直阻塞, 直到发生了指定事件
                int select = selector.select();
//                if (select > 0) {
                    System.out.println("server selector.select() = " + select);
                    // 获取所有发生了的事件, 若是select()方法已经不再阻塞
                    // 说明已经有指定事件发生, 或者wakeup()方法被调用
                    // selectedKeys()方法用于获取发生的所有事件的类型
                    selectionKeys = selector.selectedKeys();

                    for (SelectionKey selectionKey : selectionKeys) {
                        // 处理事件
                        handlers(selectionKey);
                    }
                    // 应当是需要重置Channel状态的
                    selectionKeys.clear();
//                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            close(selector);
        }
    }

    /**
     * selector处理事件
     *
     * @param selectionKey 事件
     */
    private void handlers(SelectionKey selectionKey) throws IOException {
        if (selectionKey == null){
            throw new RuntimeException("selectionKey 为空");
        }

        // 若 selectionKey 是 accept事件, 则是由ServerSocketChannel引起的
        if (selectionKey.isAcceptable()) {
            // 获取客户端channel, 注册到 selector
            ServerSocketChannel server = (ServerSocketChannel) selectionKey.channel();
            // 客户端channel
            SocketChannel client = server.accept();
            // 设置为非阻塞模式
            client.configureBlocking(false);
            // 注册
            client.register(selector, SelectionKey.OP_READ);
            log.info(getClientName(client) + " 已连接");
        }
        // 若 selectionKey 是 read 事件, 则是由 clientSocketChannel引起的
        else if (selectionKey.isReadable()) {
            // 转发消息
            // 获取客户端channel
            SocketChannel client = (SocketChannel) selectionKey.channel();
            // 获取消息
            String fwdMsg = receive(client);

            if (fwdMsg.isEmpty()) {
                // 客户端出现异常
                // 在 selector中取消该channel的注册
                selectionKey.cancel();
                // 让 尚在阻塞的select() 方法 立即返回
                // (作用在于:刷新selector需要监控的 channel)
                // 最好在每一次cancel()之后就调用一次wakeup()
                selector.wakeup();
            } else {
                // 转发消息
                forwardMassage(client, fwdMsg);

                // 退出
                if (quit(fwdMsg)) {
                    selectionKey.cancel();
                    // 作用于多线程模式, 对于单线程来说没有意义
                    selector.wakeup();
                    log.info(getClientName(client) + "已退出");
                }
            }

        }
    }

    /**
     * 转发消息
     *
     * @param client
     * @param fwdMsg
     */
    private void forwardMassage(SocketChannel client, String fwdMsg) throws IOException {
        log.info(fwdMsg);
        // 获取所有的注册在selector中的
        // 每一 channel 与 selector 对应的 SelectionKey
        for (SelectionKey key : selector.keys()) {

            Channel registeredChannel = key.channel();
            // 若是 该 SelectionKey 是 属于 ServerSocketChannel, 则 跳过
            if (registeredChannel instanceof ServerSocketChannel) {
                continue;
            }
            // 若是 该 SelectionKey 是 属于 发出消息的 channel, 则 跳过
            // isValid(), 判断 SelectionKey 是否有效
            if (key.isValid() && !registeredChannel.equals(client)) {
                // 转发
                // 转换为 写模式
                wBuffer.clear();
                // 将String转换为utf-8格式编码
                wBuffer.put(charset.encode(
                        getClientName(client) + " : " + fwdMsg));
                // 转换为 读模式
                wBuffer.flip();
                // 向通道写数据
                while (wBuffer.hasRemaining()) {
                    ((SocketChannel) key.channel()).write(wBuffer);
                }
            }
        }
    }

    public String getClientName(SocketChannel client) {
        return "客户端[ " + client.socket().getPort() + "]";
    }

    /**
     * 获取客户端的数据
     *
     * @param client
     * @return
     * @throws IOException
     */
    private String receive(SocketChannel client) throws IOException {
        // 先确保rBuffer处于写模式
        rBuffer.clear();
        // debug 模式下出错 : channel在客户端已经关闭, 所以无法读取数据
        while (client.read(rBuffer) > 0) {
        }
        // 转换为读模式
        rBuffer.flip();
        // 解码方法
        return String.valueOf(charset.decode(rBuffer));
    }

    public static void main(String[] args) {
        NIOChatServer server = new NIOChatServer(7777);
        server.start();
    }
}
